/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package net.worlds.object;

import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.SwingUtilities;
import net.worlds.WorldsException;
import net.worlds.WorldsSettings;
import net.worlds.Main;
import net.worlds.ObjectDef;
import net.worlds.ObjectList;
import net.worlds.ui.PartTreeNode;
import net.worlds.PropChangeHandler;
import net.worlds.PropertyDef;
import net.worlds.ui.UI;
import net.worlds.Util;
import net.worlds.lua.Event;
import net.worlds.lua.GameObjectFunctions;
import net.worlds.net.RemotePlayer;
import org.jbox2d.common.Vec2D;
import se.krka.kahlua.vm.JavaFunction;
import se.krka.kahlua.vm.LuaCallFrame;
import se.krka.kahlua.vm.LuaTableImpl;

/**
 *
 * @author Jacob Morgan
 */
public class GameObject {
    public net.worlds.World world;
    public boolean worldLocked = false;
    public boolean disabled = false;
    public boolean explorable = true;
    private int id = -1;
    protected final GameObject instance = this;
    protected Map<String, Object> properties;
    public Map<String, Object> tempProperties;
    private String type;
    protected ArrayList<Runnable> actionQueue = new ArrayList<Runnable>(WorldsSettings.MAX_ACTIONS);
    protected final List<GameObject> children = Collections.synchronizedList(new ArrayList<GameObject>(WorldsSettings.MAX_CHILDREN));
    private HashMap<String, PropChangeHandler> propChangeHandlers = new HashMap<String, PropChangeHandler>(WorldsSettings.MAX_PROPERTIES);
    protected PartTreeNode treeNode = new PartTreeNode(this);
    public RemotePlayer ownerPlayer = null;
    public boolean isSendable = true;
    public boolean mine = false;
    public boolean selected = false;
    public boolean newlySelected = false;
    public int originalID = -1;
    protected boolean inWorld = false;
    public boolean inUse = false;
    protected HashMap<String, JavaFunction> functions = new HashMap<String, JavaFunction>(WorldsSettings.MAX_FUNCTIONS);
    protected LinkedHashMap<String, Event> events = new LinkedHashMap<String, Event>(16);
    public Object clone(boolean replicate)
    {
        try {
            final GameObject c = world.createObject(type, -1, null, replicate);
            c.isSendable = replicate;
            for(String k : properties.keySet())
            {
                Object v = getProp(k);
                if(!k.equals("Parent"))
                    c.setProp(k, v, true);
            }
            if(c instanceof Part)
                ((Part)c).saveJoints(((Part)this));
            if(originalID == -1)
                c.originalID = id;
            else
                c.originalID = originalID;
            Runnable r = new Runnable()
            {
                public void run() {
                    List<GameObject> children = getChildren();
                    synchronized(children)
                    {
                        for(GameObject k : children)
                        {
                            try {
                                ((GameObject)k.clone(true)).setParent(c);
                            } catch (WorldsException ex) {
                                Main.log("Error cloning "+instance, ex);
                            }
                        }
                    }
                }

            };
            Main.runInMainThread(r);
            return c;
        } catch (Exception ex) {
            Main.log("Error cloning "+instance, ex);
            return null;
        }
    }
    @Override
    public Object clone()
    {
        return clone(false);
    }
    public GameObject(String type) throws WorldsException
    {
        this.type = type;
        properties = Collections.synchronizedMap(new LinkedHashMap<String, Object>(WorldsSettings.MAX_PROPERTIES));
        tempProperties = Collections.synchronizedMap(new LinkedHashMap<String, Object>(WorldsSettings.MAX_PROPERTIES));
        addPropChangeHandler("Parent", new PropChangeHandler()
        {
            @Override
            public void handle(Object oldValue, final Object value) throws WorldsException {
                onParentChanged((GameObject) oldValue, (GameObject) value);
            }
        });
        addPropChangeHandler("Name", new PropChangeHandler()
        {
            @Override
            public void handle(Object oldValue, Object value) throws WorldsException {
                addAction(new Runnable()
                {
                    public void run() {
                        SwingUtilities.invokeLater(new Runnable()
                        {
                            public void run()
                            {
                                //Main.getView().getMainFrame().getPartsTree().getModel().nodeChanged(treeNode);
                            }
                        });
                    }

                });
            }
        });
        addFunction("remove", new GameObjectFunctions.Remove());
        addFunction("isA", new GameObjectFunctions.IsA());
        addFunction("setID", new JavaFunction()
        {

            public int call(LuaCallFrame callFrame, int nArguments) {
                if(nArguments != 2 || !Util.isDouble(callFrame.get(1).toString()))
                    throw new RuntimeException("setID() requires an integer argument");
                double d = (Double) callFrame.get(1);
                if(Main.isClient())
                    setID((int) d, false);
                else
                    setID((int) d);
                return 0;
            }

        });
        addFunction("getID", new JavaFunction()
        {

            public int call(LuaCallFrame callFrame, int nArguments) {
                callFrame.push(getID());
                return 1;
            }

        });
        addFunction("clone", new JavaFunction()
        {
            public int call(LuaCallFrame callFrame, int nArguments) {
                GameObject c = (GameObject) instance.clone(true);
                callFrame.push(c);
                return 1;
            }

        });
        addFunction("getChildren", new JavaFunction()
        {

            public int call(LuaCallFrame callFrame, int nArguments) {
                LuaTableImpl t = new LuaTableImpl();
                int idx = 1;
                List<GameObject> children = getChildren();
                synchronized(children)
                {
                    for(GameObject c : getChildren())
                    {
                        t.rawset(idx, c);
                        idx++;
                    }
                }
                callFrame.push(t);
                return 1;
            }

        });
        addFunction("getChild", new JavaFunction()
        {

            public int call(LuaCallFrame callFrame, int nArguments) {
                try {
                    if (nArguments != 2 || !(callFrame.get(1) instanceof String)) {
                        throw new RuntimeException("Argument to getChild() must be a string");
                    }
                    String k = callFrame.get(1).toString();
                    if (hasChild(k))
                        callFrame.push(getChild(k));
                    else
                        callFrame.pushNil();
                    return 1;
                } catch (WorldsException ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
        addFunction("getPath", new JavaFunction()
        {

            public int call(LuaCallFrame callFrame, int nArguments) {
                callFrame.push(getFullName());
                return 1;
            }

        });
        addFunction("setOwner", new JavaFunction()
        {
            public int call(LuaCallFrame callFrame, int nArguments) {
                Player p = (Player) callFrame.get(1);
                ownerPlayer = p.getConnection().getClient();
                return 0;
            }

        });
        addFunction("getWorld", new JavaFunction()
        {
            public int call(LuaCallFrame callFrame, int nArguments) {
                if(world == null)
                    callFrame.pushNil();
                else
                    callFrame.push(world.getRootObject());
                return 1;
            }
        });
        addEvent("Removed", new Event(), "none");
        addEvent("ChildAdded", new Event(), "object (GameObject)");
        addEvent("ChildRemoved", new Event(), "object (GameObject)");
        addEvent("PropertyChanged", new Event(), "name (string), value (mixed)");
    }
    protected void beforeChangeWorld(net.worlds.World newWorld)
    {
        getEvent("Removed").disabled = true;
        for(GameObject c : getChildren())
            c.beforeChangeWorld(newWorld);
    }
    private void changeWorld(net.worlds.World newWorld)
    {
        world = newWorld;
        for(GameObject obj : children)
        {
            obj.changeWorld(newWorld);
        }
    }
    protected void finishChangeWorld()
    {
        getEvent("Removed").disabled = false;
        for(GameObject c : getChildren())
            c.finishChangeWorld();
    }
    public void replicate(final RemotePlayer ignorePlayer)
    {
        if(Main.isServer())
        {
            if(this instanceof Player)
            {
            }
            Main.getServer().sendObjectCreate(instance, ignorePlayer);
        }
    }
    public void replicateClient()
    {
        if(Main.isClient())
        {
            Runnable r = new Runnable()
            {
                public void run() {
                    Main.getClient().sendObjectCreate(instance);
                }
            };
            Main.runInMainThread(r);
        }
    }
    public void setID()
    {
        id = world.game.getFreeID();
        if(Util.isLocal(getType()))
            id *= -1;
        world.game.indexObject(this, -1);
    }
    public void setID(int id)
    {
        setID(id, true);
    }
    public void setID(int id, boolean replicate)
    {
        final int oldID = this.id;
        if(id == oldID)
            return;
        if(world.game.isIDTaken(this, id))
        {
            if(Main.debug)
            {
                System.err.println(oldID+" "+id);
                System.err.println(this+ " taken by: "+world.game.getByID(id));
            }
            world.game.getByID(id).setID();
            //id = world.game.getFreeID();
        }
        this.id = id;
        world.game.indexObject(this, oldID);
        if(!replicate || !isSendable)
            return;
        final int finalID = id;
        if(Main.isServer())
        {
            Main.getServer().sendIDChange(oldID, finalID);
        }
        else if(Main.isClient())
        {
            Runnable r = new Runnable()
            {
                public void run() {
                    Main.getClient().sendIDChange(oldID, finalID);
                }
            };
            Main.runInMainThread(r);
        }
    }
    public int getID()
    {
        return id;
    }
    protected void onParentChanged(GameObject oldValue, GameObject value) throws WorldsException
    {
        if(oldValue == value && inWorld)
            return;
        if(oldValue != null)
        {
            oldValue.removeChild(instance);
            UI.onChildRemoved(oldValue, this);
        }
        if(value == null)
            world.deleteObject(instance);
        else
        {
            if(value.world != world && world != null)
            {
                beforeChangeWorld(value.world);
                onPropChanged("Parent", getParent(), null);
                changeWorld(value.world);
                onPropChanged("Parent", getParent(), value);
                finishChangeWorld();
            }
            else
            {
                if(!((GameObject)value).getChildren().contains(this))
                    ((GameObject)value).addChild(instance);
                if(((GameObject)value).isInWorld())
                {
                    if(!inWorld)
                        try {
                            world._addObject(instance);
                        } catch (WorldsException ex) {
                            throw new RuntimeException(ex);
                        }
                    else
                    {
                        UI.onChildAdded(value, this);
                    }
                }
            }
        }
    }
    public String getType()
    {
        return type;
    }
    protected void addFunction(String n, JavaFunction f)
    {
        functions.put(n, f);
    }
    protected void addEvent(String n, Event e, String arguments)
    {
        e.eventName = n;
        events.put(n, e);
        Event.addArguments(getClass(), e.eventName, arguments);
    }
    public boolean hasEvent(String k)
    {
        return events.containsKey(k);
    }
    public Event getEvent(String k)
    {
        return events.get(k);
    }
    public boolean hasFunction(String k)
    {
        return functions.containsKey(k);
    }
    public int getNumFunctions()
    {
        return functions.size();
    }
    public JavaFunction getFunction(String k)
    {
        return functions.get(k);
    }
    public void setDefaultProperties() throws WorldsException
    {
        ObjectDef d = ObjectList.getDef(type);
        for(PropertyDef p : d.properties)
        {
            properties.put(p.name, p.defaultValue);
            if(p.defaultValue != null)
                onPropChanged(p.name, null, p.defaultValue);
        }
        setProp("Name", type, true);
        setProp("Type", type, true);
        setProp("Parent", null, false);
    }
    public void setProp(final String name, final Object value, boolean fireEvent, final RemotePlayer ignorePlayer, final boolean sendToServer) throws WorldsException
    {
        Class requiredClass = ObjectList.getPropDef(getClass().getSimpleName(), name).type;
        if(value != null && value.getClass() != requiredClass && !(value.getClass() == Integer.class && requiredClass == Float.class) && !(requiredClass == GameObject.class && value instanceof GameObject))
            throw new WorldsException(value.getClass().getSimpleName()+" is not an allowed type for "+name);
        Object oldValue = getProp(name);
        if(!properties.containsKey(name))
            throw new WorldsException(name+" is not a property of "+type);
        synchronized(properties)
        {
            properties.put(name, value);
        }
        //try
        {
            if(fireEvent)
            {
                onPropChanged(name, oldValue, value);
                if(!(this instanceof Root))
                {
                    getEvent("PropertyChanged").fire(new Object[]{name, value});
                    world.getRootObject().getEvent("PropertyChanged").fire(new Object[]{instance, name, value});
                }
            }
        }
        final boolean finalFireEvent = fireEvent;
        if(Main.isServer() && fireEvent)
        {
            Main.getServer().sendProp(instance, name, value, finalFireEvent, ignorePlayer);
        }
        else if(Main.isClient() && fireEvent && sendToServer)
        {
            Runnable r = new Runnable()
            {
                public void run() {
                    Main.getClient().sendProp(instance, name, value, finalFireEvent);
                }
            };
            Main.runInMainThread(r);
        }
    }
    /*public void finalizeProp(String name) throws BuildismException
    {
        System.err.println(name);
        setProp(name, getProp(name), true, false);
    }*/
    public void setProp(final String name, final Object value, final boolean fireEvent) throws WorldsException
    {
        setProp(name, value, fireEvent, null, true);
    }
    public void setProp(final String name, final Object value, final boolean fireEvent, final boolean sendToServer) throws WorldsException
    {
        setProp(name, value, fireEvent, null, sendToServer);
    }
    public void setProp(final String name, final Object value, final boolean fireEvent, final RemotePlayer ignorePlayer) throws WorldsException
    {
        setProp(name, value, fireEvent, ignorePlayer, true);
    }
    public void setParent(GameObject obj) throws WorldsException
    {
        setProp("Parent", obj, true);
    }
    public Object getProp(String name) throws WorldsException
    {
        synchronized(properties)
        {
            Object r = properties.get(name);
            if(properties.containsKey(name))
                return r;
            else
                throw new WorldsException(name+" is not a property of "+type);
        }
    }
    public LinkedHashMap<String, Object> getAllProperties()
    {
        LinkedHashMap<String, Object> allProps = new LinkedHashMap<String, Object>(properties.size());
        for(String k : properties.keySet())
        {
            try {
                allProps.put(k, getProp(k));
            } catch (WorldsException ex) {
                ex.printStackTrace();
            }
        }
        return allProps;
    }
    public int getIntProp(String name) throws WorldsException
    {
        return (Integer) getProp(name);
    }
    public boolean getBooleanProp(String name) throws WorldsException
    {
        return (Boolean) getProp(name);
    }
    public float getFloatProp(String name) throws WorldsException
    {
        return (Float) getProp(name);
    }
    public String getStringProp(String name) throws WorldsException
    {
        return (String) getProp(name);
    }
    public Vec2D getVec2DProp(String name) throws WorldsException
    {
        return (Vec2D) getProp(name);
    }
    public Color getColorProp(String name) throws WorldsException
    {
        return (Color) getProp(name);
    }
    public GameObject getObjectProp(String name) throws WorldsException
    {
        return (GameObject) getProp(name);
    }

    public void addAction(Runnable r)
    {
        if(inWorld)
        {
            Util.checkThread();
            r.run();
        }
        else
            actionQueue.add(r);
    }
    public boolean isInWorld()
    {
        return inWorld;
    }
    public void addToWorld() throws WorldsException
    {
        inWorld = true;
        int i = 0;
        for(Runnable a : actionQueue)
        {
            a.run();
            i++;
        }
        if(!(this instanceof Root))
            UI.onChildAdded(getParent(), instance);
        actionQueue.clear();
        for(final GameObject c : children)
        {
            try {
                if(!c.inWorld)
                    world._addObject(c);
            } catch (WorldsException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
    public void remove() throws WorldsException
    {
        setProp("Parent", null, true);
    }
    public void disable()
    {
        disabled = true;
        synchronized(children)
        {
            for(GameObject c : children)
            {
                c.disable();
            }
        }
    }
    public void enable()
    {
        disabled = false;
        synchronized(children)
        {
            for(GameObject c : children)
            {
                c.enable();
            }
        }
    }
    protected void addPropChangeHandler(String name, PropChangeHandler h)
    {
        propChangeHandlers.put(name, h);
    }
    public void onPropChanged(String name, Object oldValue, Object value) throws WorldsException
    {
        PropChangeHandler h = propChangeHandlers.get(name);
        if(h != null)
        {
            h.handle(oldValue, value);
        }
    }
    public void addChild(final GameObject c) throws WorldsException
    {
        if(children.contains(c))
            throw new WorldsException(c+" is already a child of "+this);
        children.add(c);
        if(disabled)
        {
            Runnable r = new Runnable()
            {
                public void run() {
                    c.disable();
                    if(c instanceof Part)
                        ((Part)c).getBody().resetFiltering();
                    else if(c instanceof Model)
                        ((Model)c).resetFiltering();
                }
            };
            if(c.isInWorld())
                r.run();
            else
                Main.addStepTask(r);
        }
        getEvent("ChildAdded").fire(new Object[]{c});
    }
    public void insertChildAt(GameObject c, int pos)
    {
        children.add(pos, c);
    }
    public void removeChild(final GameObject c)
    {
        //if(children.contains(c))
        {
            children.remove(c);
            getEvent("ChildRemoved").fire(new Object[]{c});
        }
        if(c.disabled)
        {
            Runnable r = new Runnable()
            {
                public void run() {
                    c.enable();
                    if(c instanceof Part)
                        ((Part)c).getBody().resetFiltering();
                    else if(c instanceof Model)
                        ((Model)c).resetFiltering();
                }
            };
            if(c.isInWorld())
                r.run();
            else
                Main.addStepTask(r);
        }
    }
    public boolean hasChild(String name)
    {
        synchronized(children)
        {
            for(GameObject c : children)
            {
                if(c.toString().equals(name))
                    return true;
            }
        }
        return false;
    }
    public GameObject getChild(String name) throws WorldsException
    {
        synchronized(children)
        {
            for(GameObject c : children)
            {
                if(c.getStringProp("Name").equals(name))
                    return c;
            }
        }
        throw new WorldsException(name+" is not a child of "+this);
    }
    public List<GameObject> getChildren()
    {
        return children;
    }
    public GameObject getParent() throws WorldsException
    {
        return getObjectProp("Parent");
    }
    public void setTreeNode(PartTreeNode n)
    {
        treeNode = n;
    }
    public PartTreeNode getTreeNode()
    {
        return treeNode;
    }
    public void destructor() throws WorldsException
    {
        inWorld = false;
    }
    public void step() throws WorldsException
    {
        
    }
    public void draw(Graphics g) throws WorldsException
    {
        
    }
    public String getFullName()
    {
        String path = toString();
        GameObject obj = this;
        /*while(obj.getParent() != null)
        {
            obj = obj.getParent();
            path = obj.toString()+"."+path;
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
            }
            System.err.println(Main.getActiveWorld().getRootObject().getParent());
        }*/
        return path;
    }
    public void setInUse(boolean v)
    {
        Util.checkThread();
        inUse = v;
        if(this instanceof Part)
        {
            try {
                ((Part) this).setTempFixed(v);
            } catch (WorldsException ex) {
                ex.printStackTrace();
            }
        }
        if(Main.isServer())
            Main.getServer().sendInUse(this, v);
        else if(Main.isClient())
            Main.getClient().sendInUse(this, v);
    }
    public Collection<Event> getEvents()
    {
        return events.values();
    }
    @Override
    public String toString()
    {
        try {
            return getStringProp("Name");
        } catch (WorldsException ex) {
            return "";
        }
    }
}
